/*
 * BlackLightning.PacketConnection
 * Andy Tidball
 * 
 * Project: Black Lightning
 * Copyright: GNU General Public License
 */

using System;
using System.Net.Sockets;
using System.Text;

namespace BLLib.Net {
	/// <summary>
	/// A connection which sends and receives discreet packets.  Also translates newlines from the other end to Environment.NewLine on this end.
	/// </summary>
	public class PacketConnection {
		// member data
		private Connection    _Connection;       // the connection packets are sent and received through
		private string        _PacketTerminator; // the string which signals the end of a received packet
		private StringBuilder _ReceiveBuffer;    // data received from the connection which isn't yet a full packet
		private bool          _AppendTerminator; // whether or not to append the packet terminator to outgoing packets
		private string        _SendNewLine;      // the string that the other end of the connection recognizes as a new line when we send to it
		private string        _ReceiveNewLine;   // the string that the other end of the connection recognizes as a new line when we receive from it

		/// <summary>
		/// Creates a new PacketConnection.
		/// </summary>
		/// <param name="Connection">A connection which is already connected.</param>
		/// <param name="PacketTerminator">The string which marks the end of a packet.</param>
		public PacketConnection(Connection Connection, string PacketTerminator)
			: this(Connection, PacketTerminator, true, Environment.NewLine, Environment.NewLine) {
		}

		/// <summary>
		/// Creates a new PacketConnection.
		/// </summary>
		/// <param name="Connection">A connection which is already connected.</param>
		/// <param name="PacketTerminator">The string which marks the end of a packet.</param>
		/// <param name="AppendTerminator">Whether or not to append the packet terminator to outgoing packets that don't already end with it.</param>
		public PacketConnection(Connection Connection, string PacketTerminator, bool AppendTerminator)
			: this(Connection, PacketTerminator, AppendTerminator, Environment.NewLine, Environment.NewLine) {
		}

		/// <summary>
		/// Creates a new PacketConnection.
		/// </summary>
		/// <param name="Connection">A connection which is already connected.</param>
		/// <param name="PacketTerminator">The string which marks the end of a packet.</param>
		/// <param name="NewLine">The string that the other end of the connection recognizes as a new line.</param>
		public PacketConnection(Connection Connection, string PacketTerminator, string NewLine)
			: this(Connection, PacketTerminator, true, NewLine, NewLine) {
		}

		/// <summary>
		/// Creates a new PacketConnection.
		/// </summary>
		/// <param name="Connection">A connection which is already connected.</param>
		/// <param name="PacketTerminator">The string which marks the end of a packet.</param>
		/// <param name="AppendTerminator">Whether or not to append the packet terminator to outgoing packets that don't already end with it.</param>
		/// <param name="SendNewLine">The string that the other end of the connection recognizes as a new line when we're sending to it.</param>
		/// <param name="ReceiveNewLine">The string that the other end of the connection recognizes as a new line when we're receiving from it.</param>
		public PacketConnection(Connection Connection, string PacketTerminator, bool AppendTerminator, string SendNewLine, string ReceiveNewLine) {
			_Connection = Connection;
			_PacketTerminator = PacketTerminator;
			_ReceiveBuffer = null;
			_AppendTerminator = AppendTerminator;
			_SendNewLine = SendNewLine;
			_ReceiveNewLine = ReceiveNewLine;
		}

		/// <summary>
		/// Creates a new PacketConnection that has no connection.
		/// </summary>
		/// <param name="PacketTerminator">The string which marks the end of a packet.</param>
		protected PacketConnection(string PacketTerminator)
			: this(PacketTerminator, true) {
		}

		/// <summary>
		/// Creates a new PacketConnection that has no connection.
		/// </summary>
		/// <param name="PacketTerminator">The string which marks the end of a packet.</param>
		/// <param name="AppendTerminator">Whether or not to append the packet terminator to outgoing packets that don't already end with it.</param>
		protected PacketConnection(string PacketTerminator, bool AppendTerminator)
			: this(PacketTerminator, AppendTerminator, Environment.NewLine, Environment.NewLine) {
		}

		/// <summary>
		/// Creates a new PacketConnection that has no connection.
		/// </summary>
		/// <param name="PacketTerminator">The string which marks the end of a packet.</param>
		/// <param name="NewLine">The string that the other end of the connection recognizes as a new line.</param>
		protected PacketConnection(string PacketTerminator, string NewLine)
			: this(PacketTerminator, true, NewLine, NewLine) {
		}

		/// <summary>
		/// Creates a new PacketConnection that has no connection.
		/// </summary>
		/// <param name="PacketTerminator">The string which marks the end of a packet.</param>
		/// <param name="AppendTerminator">Whether or not to append the packet terminator to outgoing packets that don't already end with it.</param>
		/// <param name="SendNewLine">The string that the other end of the connection recognizes as a new line when we're sending to it.</param>
		/// <param name="ReceiveNewLine">The string that the other end of the connection recognizes as a new line when we're receiving from it.</param>
		protected PacketConnection(string PacketTerminator, bool AppendTerminator, string SendNewLine, string ReceiveNewLine)
			: this(null, PacketTerminator, AppendTerminator, SendNewLine, ReceiveNewLine) {
		}

		/// <summary>
		/// Gets whether or not the connection is currently connected.
		/// </summary>
		public bool Connected {
			get {
				return _Connection.Connected;
			}
		}

		/// <summary>
		/// Gets or sets the string which separates packets.
		/// </summary>
		public string PacketTerminator {
			get {
				return _PacketTerminator;
			}
			set {
				_PacketTerminator = value;
			}
		}

		/// <summary>
		/// Gets or sets the string recognized on the other end of the Connection as a new line when we send to it.
		/// </summary>
		public string SendNewLine {
			get {
				return _SendNewLine;
			}
			set {
				_SendNewLine = value;
			}
		}

		/// <summary>
		/// Gets or sets the string recognized on the other end of the Connection as a new line when we receive from it.
		/// </summary>
		public string ReceiveNewLine {
			get {
				return _ReceiveNewLine;
			}
			set {
				_ReceiveNewLine = value;
			}
		}

		/// <summary>
		/// Raised when a packet is received from the connection.
		/// </summary>
		public event PacketReceivedEventHandler PacketReceived;

		/// <summary>
		/// Raised when the connection is lost, voluntarily or not.
		/// </summary>
		public event DisconnectedEventHandler Disconnected;

		/// <summary>
		/// Sends the given Packet without any contextual information.
		/// </summary>
		/// <param name="Packet">The packet to send.</param>
		public virtual void SendPacket(Packet Packet) {
			SendPacket(Packet, null);
		}

		/// <summary>
		/// Sends the given Packet across the connection.
		/// </summary>
		/// <param name="Packet">The Packet to send.</param>
		/// <param name="Context">Context to use to create the packet's output data.</param>
		public void SendPacket(Packet Packet, PacketContext Context) {
			try {
				// create the packet's output
				string Output = Packet.CreateOutput(Context);

				// if the other end doesn't use the same new line we do, do a replacement
				if (_SendNewLine != Environment.NewLine) {
					Output = Output.Replace(Environment.NewLine, _SendNewLine);
				}

				// append the packet terminator if needed
				if (!Output.EndsWith(PacketTerminator) && _AppendTerminator) {
					Output += PacketTerminator;
				}

				// send the data
				_Connection.Send(Output);
			} catch (SocketException x) {
				OnDisconnected(new DisconnectedEventArgs(x));
			}
		}

		/// <summary>
		/// Checks the connection for incoming packets and raises PacketReceived events for any complete ones that have arrived.
		/// </summary>
		public virtual void CheckForPackets() {
			try {
				ProcessReceivedData(_Connection.Receive());
			} catch (SocketException x) {
				OnDisconnected(new DisconnectedEventArgs(x));
			}
		}

		/// <summary>
		/// Disconnects the connection.
		/// </summary>
		public virtual void Disconnect() {
			if (!_Connection.Connected) {
				return;
			}

			_Connection.Disconnect();
			OnDisconnected(new DisconnectedEventArgs(null));
		}

		/// <summary>
		/// Gets or sets the connection which packets are sent and received on.
		/// </summary>
		protected Connection Connection {
			get {
				return _Connection;
			}
			set {
				_Connection = value;
			}
		}

		/// <summary>
		/// Raises the PacketReceived event.
		/// </summary>
		/// <param name="e">Arguments to send to the event.</param>
		protected void OnPacketReceived(PacketReceivedEventArgs e) {
			if (PacketReceived != null) {
				PacketReceived(this, e);
			}
		}

		/// <summary>
		/// Raises the Disconnected event.
		/// </summary>
		/// <param name="e">Arguments to send to the event.</param>
		protected void OnDisconnected(DisconnectedEventArgs e) {
			if (_Connection.Connected) {
				_Connection.Disconnect();
			}
			if (Disconnected != null) {
				Disconnected(this, e);
			}
		}

		/// <summary>
		/// Creates a new RawPacket with the given data.
		/// </summary>
		/// <param name="Data">The data to include in the new Packet.</param>
		protected virtual Packet CreatePacket(string Data) {
			return new RawPacket(Data, null);
		}

		/// <summary>
		/// Processes incoming data into the receive buffer.
		/// </summary>
		/// <param name="Data">The incoming data to process.</param>
		private void ProcessReceivedData(string Data) {
			// if we didn't receive any data, then we got disconnected
			if (Data == "") {
				throw new SocketException(10054);
			}

			// split the data along packet terminators
			string PacketData = Data;
			string Remainder  = "";
			int    SplitIndex = PacketData.IndexOf(_PacketTerminator);

			// keep going as long as we find more terminators
			while (SplitIndex >= 0) {
				// split the data at the appropriate index
				if (SplitIndex == 0) {
					// packet terminator is the first thing in the data
					Remainder  = PacketData.Substring(_PacketTerminator.Length);
					PacketData = PacketData.Substring(0, _PacketTerminator.Length);
				} else if (SplitIndex == (PacketData.Length - _PacketTerminator.Length)) {
					// packet terminator is the last thing in the data
					Remainder  = "";
					//PacketData = PacketData;
				} else {
					// packet terminator is somewhere in the middle of the data
					Remainder  = PacketData.Substring(SplitIndex + _PacketTerminator.Length);
					PacketData = PacketData.Substring(0, SplitIndex + _PacketTerminator.Length);
				}

				// append the packet data to the buffer and create a packet to send
				if (PacketData != "") {
					AppendReceivedData(PacketData);
				}
				OnPacketReceived(new PacketReceivedEventArgs(CreatePacketAndFlushBuffer()));

				// move the remainder over and check for a new split index
				if (Remainder == "") {
					PacketData = "";
					SplitIndex = -1;
				} else {
					PacketData = Remainder;
					SplitIndex = PacketData.IndexOf(_PacketTerminator);
				}
			}

			// append whatever remains in the packet data
			if (PacketData != "") {
				AppendReceivedData(PacketData);
			}
		}
		
		/// <summary>
		/// Appends the given string to the receive buffer.
		/// </summary>
		/// <param name="Data">The data to append to the receive buffer.</param>
		private void AppendReceivedData(string Data) {
			// if the other end doesn't use the same new line we do, do a replacement
			if (_ReceiveNewLine != Environment.NewLine) {
				Data = Data.Replace(_ReceiveNewLine, Environment.NewLine);
			}

			// append the data to the receive buffer
			if (_ReceiveBuffer == null) {
				_ReceiveBuffer = new StringBuilder(Data);
			} else {
				_ReceiveBuffer.Append(Data);
			}
		}

		/// <summary>
		/// Creates a new packet from the receive buffer and then clears the buffer.
		/// </summary>
		/// <returns>A new packet created with the data in the receive buffer.</returns>
		private Packet CreatePacketAndFlushBuffer() {
			Packet P = CreatePacket(_ReceiveBuffer.ToString());
			_ReceiveBuffer = null;
			return P;
		}
	}

	/// <summary>
	/// The prototype for a PacketReceived event.
	/// </summary>
	/// <param name="Sender">The object which received the packet.</param>
	/// <param name="e">Arguments related to the packet received.</param>
	public delegate void PacketReceivedEventHandler(object Sender, PacketReceivedEventArgs e);

	/// <summary>
	/// The arguments sent with a PacketReceived event.
	/// </summary>
	public class PacketReceivedEventArgs : EventArgs {
		// member data
		private Packet _Packet; // the packet which was received

		/// <summary>
		/// Creates a new set of PacketReceivedEventArgs.
		/// </summary>
		/// <param name="Packet">The packet that was received.</param>
		public PacketReceivedEventArgs(Packet Packet) {
			_Packet = Packet;
		}

		/// <summary>
		/// The packet that was received.
		/// </summary>
		public Packet Packet {
			get {
				return _Packet;
			}
		}
	}

	/// <summary>
	/// The prototype for a Disconnected event.
	/// </summary>
	/// <param name="Sender">The object which was disconnected.</param>
	/// <param name="e">Arguments related to the disconnection.</param>
	public delegate void DisconnectedEventHandler(object Sender, DisconnectedEventArgs e);

	/// <summary>
	/// The arguments sent with a Disconnected event.
	/// </summary>
	public class DisconnectedEventArgs : EventArgs {
		// member data
		private SocketException _Exception; // the exception which indicated a disconnection

		/// <summary>
		/// Creates a new set of GameServerDisconnectedEventArgs.
		/// </summary>
		/// <param name="Exception">The exception which indicated a disconnection.</param>
		public DisconnectedEventArgs(SocketException Exception) {
			_Exception = Exception;
		}

		/// <summary>
		/// The exception which indicated a disconnection, or null if the disconnection was manual.
		/// </summary>
		public SocketException Exception {
			get {
				return _Exception;
			}
		}
	}
}
